#include "stdafx.h"
#include <sstream>
#include <math.h>
#include "TpcCatalog.h"
using namespace Tpc;
#include "TpqMapManager.h"

TpqMapManager* TpqMapManager::instance;

TpqMapManager::TpqMapManager(void)
{
	moveStep = 40;
	mapLevel = 3;
	if (instance != nullptr)
		throw "Only one instance of TpqMapManager allowed!";
	instance = this;

	presetScales.resize(5);

	// Manually calculated scales to minimizing rounding errors.
	// The increment is 2^(1/3)
	presetScales[2].push_back(-960.0/3600);

	presetScales[3].push_back(-2495.0/1800); // 1.747
	presetScales[3].push_back(-3144.0/1800); // 1.747
	presetScales[3].push_back(-3961.0/1800); // 2.201

	// presetScales[4].push_back(-1015.0/450); // 2.256
	presetScales[4].push_back(-1280.0/450);
	presetScales[4].push_back(-1613.0/450);
	presetScales[4].push_back(-2032.0/450);
	presetScales[4].push_back(-2560.0/450);
	presetScales[4].push_back(-3225.0/450);
	presetScales[4].push_back(-4063.0/450);
}

GSize<double> TpqMapManager::GetMapSize(int level) const
{
	GSize<double> mapSize; // Map size in world coordinate

	switch (level)
	{
	case (3):
		mapSize.height = 60*60;
		mapSize.width = 60*60;
		break;
	case (4):
		mapSize.height = 30*60;
		mapSize.width = 60*60;
		break;
	case (5):
		mapSize.height = 7.5*60;
		mapSize.width = 7.5*60;
		break;
	default:
		break;
	}

	return mapSize;
}

double TpqMapManager::PickClosestScale(int level, double s)
{
	double result = *(presetScales[level-1].begin());
	for (vector<double>::iterator i = presetScales[level-1].begin(); i < presetScales[level-1].end()-1; i ++)
	{
		if (-s < sqrt((*i) * (*(i+1))))
			break;
		else
			result = *(i+1);
	}
	return result;
}

double TpqMapManager::GetNextScale(double scale, int step)
{
	vector<double>::iterator i = find(presetScales[mapLevel-1].begin(), presetScales[mapLevel-1].end(), scale);
	if (i == presetScales[mapLevel-1].end())
		throw "Scale is not in presetScales[]!";

	if (i == presetScales[mapLevel-1].begin() && step < 0)
		return scale;

	vector<double>::iterator result = i+step;
	if (result < presetScales[mapLevel-1].begin())
		result = presetScales[mapLevel-1].begin();
	if (result >= presetScales[mapLevel-1].end())
		result = presetScales[mapLevel-1].end()-1;

	if (result == i)
		return scale;

	return *result;
}

TpqMapManager& TpqMapManager::Instance()
{
	if (instance == nullptr)
		throw "TpqMapManager instance referenced before initialized!";
	return *instance;
}

void TpqMapManager::Initialize(CDC& dc, LatLongCoord center, double desiredScale)
{
	loadCatalogs("C:\\Program Files (x86)\\TOPO!\\TPO_DATA\\Catalogs", mapCatalogs);
	//loadCatalogs("D:\\temp", mapCatalogs);

	// TODO: determine map level based on desired level
	this->center = center;

	// TODO: handle the exception here!
	MapId id = LookupMapId(center, mapLevel);
	shared_ptr<TpqMap> map = LoadMap(dc, id);

	// Now calculate the appropriate x and y scales based on the native resolution and desired scale
	nativeScale		= map->GetNativeScale();
	scale.second	= PickClosestScale(mapLevel, nativeScale.second);
	scale.first		= scale.second/nativeScale.second*nativeScale.first;
}

shared_ptr<TpqMap> TpqMapManager::LoadMap(CDC& dc, MapId id)
{
	map<MapId, shared_ptr<TpqMap>>::iterator i = maps.find(id);
	if (i == maps.end())
	{
		shared_ptr<TpqMap> newMap (new TpqMap);
		try
		{
			newMap->LoadFile(dc, GetFilePath(id));
			maps.insert(pair<MapId, shared_ptr<TpqMap>>(id,newMap));
		}
		catch (string s)
		{
			// TODO: Log the error message
			// Add an nullptr entry to the map table anyway, to prevent further attempt to load the file.
			maps[id] = nullptr;
		}
		return maps[id];
	}
	else
		return i->second;
}

MapId TpqMapManager::LookupMapId(LatLongCoord coord, int level) const
{
	GSize<double> mapSize = GetMapSize(level);

	MapId result;
	result.level	= level;
	result.east		= (int)ceil(coord.longitude/mapSize.width)*mapSize.width;
	result.south	= (int)floor(coord.latitude/mapSize.height)*mapSize.height;

	return result;
}

string TpqMapManager::GetFilePath(MapId id) const
{
	int latitudeDegree	= id.south/3600;
	int longitudeDegree	= -id.east/3600;
	char latitudeBlock	= 'a'+int(fmod(id.south, 3600)/450);
	int longitudeBlock	= 1+fmod(-id.east, 3600)/450;

	// TODO: case sensitivity??
	string prefix;
	if (id.level == 5)
		prefix = "q";
	else if (id.level == 4)
		prefix = "k";
	else if (id.level == 3)
		prefix = "g";

	ostringstream stream;
	stream << prefix << latitudeDegree << longitudeDegree << latitudeBlock << longitudeBlock;
	const string baseFileName = stream.str();
	map<string, string>::const_iterator iterator = mapCatalogs.find(baseFileName);
	if (iterator == mapCatalogs.end())
		return nullptr;
	else 
		return "D:\\Topo Data\\" + iterator->second + baseFileName + ".tpq";
}

void TpqMapManager::Pan(CDC& dc, int xStep, int yStep)
{
	center.longitude += xStep * round(moveStep/scale.first);
	center.latitude += yStep * round(moveStep/scale.second);

	MapId id = LookupMapId(center, mapLevel);
	shared_ptr<TpqMap> map = LoadMap(dc, id);
	if (map != nullptr)
	{
		pair<double, double> newScale = map->GetNativeScale();
		if (newScale != nativeScale)
		{
			// Panning should not change yScale.
			nativeScale	= newScale;
			scale.first	= scale.second/nativeScale.second*nativeScale.first;
		}
	}
}

void TpqMapManager::Zoom(int step)
{
	double newYScale = GetNextScale(scale.second, step);
	if (newYScale != scale.second)
	{
		scale.second = newYScale;
		MapId id = LookupMapId(center, mapLevel);
		scale.first	= scale.second/nativeScale.second*nativeScale.first;
	}
}

void TpqMapManager::Draw(CDC& dc, CRect drawAreaScreen)
{
	Draw(dc, drawAreaScreen, this->scale, false);
}

void TpqMapManager::Draw(CDC& dc, CRect drawAreaScreen, pair<double,double> scale, bool loadCacheOnly)
{
	GSize<double> mapSize = GetMapSize(mapLevel);
	GRect<double> rectWorld;
	rectWorld.north	= center.latitude - drawAreaScreen.Height()/scale.second/2;
	rectWorld.west	= center.longitude - drawAreaScreen.Width()/scale.first/2;
	rectWorld.south	= center.latitude + drawAreaScreen.Height()/scale.second/2;
	rectWorld.east	= center.longitude + drawAreaScreen.Width()/scale.first/2;

	Transformation worldToScreen;
	worldToScreen.yScale = scale.second;
	worldToScreen.xScale = scale.first;
	worldToScreen.xDelta = (drawAreaScreen.left*rectWorld.east - drawAreaScreen.right*rectWorld.west)/(rectWorld.east - rectWorld.west);
	worldToScreen.yDelta = (drawAreaScreen.bottom*rectWorld.north - drawAreaScreen.top*rectWorld.south)/(rectWorld.north - rectWorld.south);

	int minCol = floor(rectWorld.west/mapSize.width);
	int maxCol = ceil(rectWorld.east/mapSize.width)-1;
	int maxRow = ceil(rectWorld.north/mapSize.height)-1;
	int minRow = floor(rectWorld.south/mapSize.height);

	for (int row = minRow; row <= maxRow; row ++)
		for (int col = minCol; col <= maxCol; col ++)
		{
			GRect<double> thisGrid;
			
			thisGrid.east	= (col+1)*mapSize.width;
			thisGrid.west	= col*mapSize.width;
			thisGrid.south	= row*mapSize.height;
			thisGrid.north	= (row+1)*mapSize.height;

			shared_ptr<TpqMap> map = LoadMap(dc, LookupMapId(LatLongCoord(thisGrid.south, thisGrid.east), mapLevel));
			
			GRect<double> chunkWorld = rectWorld & thisGrid;
			if (map == nullptr)
			{
				if (!loadCacheOnly)
				{
					CRect screen;
					screen.left		= round(chunkWorld.west * worldToScreen.xScale + worldToScreen.xDelta);
					screen.right	= round(chunkWorld.east * worldToScreen.xScale + worldToScreen.xDelta);
					screen.top		= round(chunkWorld.south * worldToScreen.yScale + worldToScreen.yDelta);
					screen.bottom	= round(chunkWorld.north * worldToScreen.yScale + worldToScreen.yDelta);

					dc.FillRect(screen, &CBrush(RGB(128,128,128)));
				}
			}
			else
				map->Draw(dc, chunkWorld, worldToScreen, loadCacheOnly);
		}
}

void TpqMapManager::DrawUTMGrids(CDC& dc, GRect<double> rectWorld, Transformation wolrdToScreen) const
{
	// Now compute the UTM coordinates of all 4 corners
	UtmCoord utmnw (LatLongCoord(rectWorld.north, rectWorld.west));
	UtmCoord utmse (LatLongCoord(rectWorld.south, rectWorld.east));
	UtmCoord utmsw (LatLongCoord(rectWorld.south, rectWorld.west));
	UtmCoord utmne (LatLongCoord(rectWorld.north, rectWorld.east));

	double maxnorthing	= ceil(max(utmnw.northing, utmne.northing)/1000);
	double minnorthing	= floor(min(utmsw.northing, utmse.northing)/1000);
	double maxeasting	= ceil(max(utmse.easting, utmne.easting)/1000);
	double mineasting	= floor(max(utmnw.easting, utmsw.easting)/1000);

	int zone = utmnw.zone;

	for (double easting = mineasting; easting <= maxeasting; easting ++)
	{
		LatLongCoord t (UtmCoord(easting*1000, maxnorthing*1000, zone));
		LatLongCoord b (UtmCoord(easting*1000, minnorthing*1000, zone));

		dc.MoveTo(round(t.longitude * wolrdToScreen.xScale + wolrdToScreen.xDelta), round(t.latitude * wolrdToScreen.yScale + wolrdToScreen.yDelta));
		dc.LineTo(round(b.longitude * wolrdToScreen.xScale + wolrdToScreen.xDelta), round(b.latitude * wolrdToScreen.yScale + wolrdToScreen.yDelta));
	}

	for (double northing = minnorthing; northing <= maxnorthing; northing ++)
	{
		LatLongCoord l (UtmCoord(mineasting*1000, northing*1000, zone));
		LatLongCoord r (UtmCoord(maxeasting*1000, northing*1000, zone));

		dc.MoveTo(round(l.longitude * wolrdToScreen.xScale + wolrdToScreen.xDelta), round(l.latitude * wolrdToScreen.yScale + wolrdToScreen.yDelta));
		dc.LineTo(round(r.longitude * wolrdToScreen.xScale + wolrdToScreen.xDelta), round(r.latitude * wolrdToScreen.yScale + wolrdToScreen.yDelta));
	}
}

void TpqMapManager::PutRenderedMapletInCache(RenderedMapletMap::key_type key, RenderedMapletMap::mapped_type value)
{
	renderedMaplets.insert(RenderedMapletMap::value_type(key,value));
}

void TpqMapManager::PutPendingMapletInQueue(RenderedMapletMap::key_type key, RenderedMapletMap::mapped_type value)
{
	pendingMaplets.push(RenderedMapletMap::value_type(key,value));
}

shared_ptr<RenderedMaplet> TpqMapManager::FindMapletInCache(RenderedMapletMap::key_type key, double xScale, double yScale) const
{
	shared_ptr<RenderedMaplet> bestMatch = nullptr;
	pair<RenderedMapletMap::const_iterator, RenderedMapletMap::const_iterator> range = renderedMaplets.equal_range(key);
	
	for (RenderedMapletMap::const_iterator i = range.first; i != range.second; i ++)
	{
		if (i->second->yScale == yScale)
		{
			// Exact match found, no need to continue searching.
			bestMatch = i->second;
			break;
		}

		if (bestMatch == nullptr)
		{
			bestMatch = i->second;
			continue;
		}

		if (abs(log(i->second->yScale/yScale)) < abs(log(bestMatch->yScale/yScale)))
		{
			// Current's yScale is better than the best match found so far.
			bestMatch = i->second;
			continue;
		}
	}
	return bestMatch;
}

void TpqMapManager::RenderPendingMaplets(CDC& dc)
{
	while (!pendingMaplets.empty())
	{
		RenderedMapletMap::value_type next = pendingMaplets.front();
		// The map should have already been loaded (or attempt has failed). So there is no need to load it.
		if (maps[next.first.id] != nullptr)
		{
			maps[next.first.id]->RenderMaplet(dc, next.first, *(next.second));
		}
		pendingMaplets.pop();
		renderedMaplets.insert(next);
	}
}

void TpqMapManager::PreloadCache(CDC& dc, CRect drawAreaScreen)
{
	pair<double,double> newScale;
	newScale.second = GetNextScale(scale.second, -1);
	if (newScale.second != scale.second)
	{
		newScale.first	= newScale.second/nativeScale.second*nativeScale.first;
		Draw(dc, drawAreaScreen, newScale, true);
	}

	newScale.second = GetNextScale(scale.second, 1);
	if (newScale.second != scale.second)
	{
		newScale.first	= newScale.second/nativeScale.second*nativeScale.first;
		Draw(dc, drawAreaScreen, newScale, true);
	}
}

void TpqMapManager::OnIdle(CView& view)
{
	CDC* dc = view.GetDC();
	// Tasks to execute in order of priority:
	if (!pendingMaplets.empty())
	{
		// 1. Render pending maplets, they will have an immediate visual impact.
		
		RenderPendingMaplets(*dc);
		view.Invalidate(FALSE);
		// Return now to refresh the view. Other tasks will be executed on the next entry.
	}
	else
	{
		// 2. Load native maplets, they are needed whenever we pan or zoom.
		for (map<MapId, shared_ptr<TpqMap>>::iterator i = maps.begin(); i != maps.end(); i ++)
		{
			shared_ptr<TpqMap> m = i->second;
			if (m != nullptr && m->HasPendingNativeMaplets())
				m->LoadAllNativeMaplets();
		}

		// 3. Load cache. This will potentially speed up zooming and panning.
		CRect rect;
		view.GetClientRect(rect);
		PreloadCache(*dc, rect);
	}
}